# Copyright 2007 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.


import email.Charset
import email.FeedParser
import email.Utils
import smtplib
import sys

import gvn.changebranch
import gvn.cmdline
import gvn.description
import gvn.diff
import gvn.subcommands.change
import gvn.util


helptext__gvn_mail = """mail: Mail a changebranch review request.
usage: mail [--reviewers REVIEWER,...] [--cc CC,...] -c CHANGE | PATH...

If CHANGE does not exist, create it based on PATHs.  If CHANGE is
unspecified, generate a random name for the new changebranch.
"""


def AppendEmailDomain(pconfig, username):
  """Append @pconfig.email_domain if not None, and if not already there."""

  if pconfig.email_domain is None:
    return username
  if username.find('@') >= 0:
    return username
  return '@'.join([username, pconfig.email_domain])

def GetDiffSizeDesc(diff_size):
  """Provide text describing the size of the code review."""

  if diff_size == 0:
    desc = "a code review of unknown size"
  elif diff_size < 2:
    desc = "a wee code review"
  elif diff_size < 5:
    desc = "a tiny code review"
  elif diff_size < 30:
    desc = "a small code review"
  elif diff_size < 100:
    desc = "a medium-size code review"
  elif diff_size < 1000:
    desc = "a code review"
  elif diff_size < 2000:
    desc = "a freakin huge code review"
  elif diff_size < 3000:
    desc = "a jupiterian code review"
  elif diff_size < 4000:
    desc = "a month long code review"
  elif diff_size < 5000:
    desc = "a whopping big code review"
  elif diff_size < 10000:
    desc = "the mother of all code reviews"
  elif diff_size < 20000:
    desc = "the grandmother of all code reviews"
  else:
    desc = "a category 5 code review"
  return desc

def ParseMail(pconfig, message):
  """Return ([recipient addresses], utf-8-encoded message str) from message.

  Parse recipient addresses from message, then remove any BCC headers from
  the returned message str.

  Arguments:
  pconfig -- gvn.config.ProjectConfig
  message -- unicode text of the message
  """

  # Pass unicode message in...
  parser = email.FeedParser.FeedParser()
  parser.feed(message.encode('utf-8'))
  message = parser.close()

  recipients = []
  for header in 'to', 'cc', 'bcc':
    for (name, address) in email.Utils.getaddresses(message.get_all(header,
                                                                    [])):
      recipients.append(AppendEmailDomain(pconfig, address))

  del message['bcc']

  message['User-Agent'] = 'gvn/' + gvn.VERSION

  # Here's what we have to do to send 8bit, non-base64, utf-8 text.
  email.Charset.add_charset('utf-8', header_enc=email.Charset.SHORTEST,
                            body_enc=None, output_charset='utf-8')
  message.set_charset('utf-8')

  # ...get RFC2047-encoded UTF-8 str back.
  return (recipients, message.as_string())

def Handle_GvnMail(ctx):
  pconfig = ctx.project_config
  project = ctx.project

  recipients = {}
  ccrecipients = {}

  # TODO(epg): should we handle this list parsing stuff where we
  # process the ctx.options?  I think so.
  if ctx.options.reviewers is not None:
    for recipient in ctx.options.reviewers.split(','):
      recipients[recipient] = 1
  if ctx.options.cc is not None:
    for ccrecipient in ctx.options.cc.split(','):
      ccrecipients[ccrecipient] = 1
  recipients = list(recipients.iterkeys())
  ccrecipients = list(ccrecipients.iterkeys())

  cb = None
  if ctx.options.change is not None:
    (username, name, revision) = gvn.util.ParseChangeName(ctx.options.change)
    cb = gvn.changebranch.ChangeBranch(ctx.config, project, name,
                                       username, revision)
    if not cb.Exists():
      cb = None
  if cb is None:
    cb =  gvn.subcommands.change.Handle_GvnChange(ctx)
    if cb is None:
      return 1

  if project.diff_lines > 0:
    # TODO(epg): Should return unicode, today returns str in user's
    # default encoding.
    (shortdiff, diff_size) = gvn.diff.GetShortDiff(project, cb, ctx.pool)
  else:
    shortdiff = ''
    diff_size = 0
  diff_size_desc = GetDiffSizeDesc(diff_size)

  fromaddr = AppendEmailDomain(pconfig, pconfig.email_address)

  toaddrs = [AppendEmailDomain(pconfig, r) for r in recipients]
  ccaddrs = [AppendEmailDomain(pconfig, r) for r in ccrecipients]
  desc_object = gvn.description.Description(ctx.config, project,
                                            cb.change_name)
  cb_desc = desc_object.Output()
  short_desc = desc_object.Log()
  if len(short_desc) > 39:
    short_desc = short_desc[:36] + '...'
  short_desc = short_desc.replace('\n', ' ')

  template_dict = {
    'project': project.repository.URL(project.path),
    'reviewers': ', '.join(recipients),
    'change_name': cb.change_name,
    'change_name_at_head': cb.change_name_at_head,
    'description': cb_desc,
    'short_desc': short_desc,
    'shortdiff': shortdiff,
    'diff_size_desc': diff_size_desc,
    'diff_size': diff_size,
  }
  mail_template_name = project.mail_template
  message = '\n'.join([
    'From: ' + fromaddr,
    'To: ' + ', '.join(toaddrs),
    'Cc: ' + ', '.join(ccaddrs),
    project.mail_template % template_dict,
    ])

  # On Windows, the interaction among Python, Cygwin or the cmd shell, and
  # various versions of diff can be a mess, leaving the message with mixed
  # line endings that confuse some editors and either drop or double endings
  # in the final mail.  We convert them all to Unix endings (\n) here, and let
  # Python and/or Cygwin handle converting that to the OS's ending when they
  # write the file out.
  # Assume endings are some combination of \r, \n, and \r\n.
  if sys.platform == 'win32':
    message = message.replace('\r\n', '\n').replace('\r', '\n')

  editor = None
  try:
    if ctx.options.non_interactive:
      # Just send unedited form to --reviewers + --cc optargs.
      toaddrs.extend(ccaddrs)
    else:
      # Run the user's editor on the form, then send edited form to
      # addresses parsed from the edited form.
      editor = gvn.util.Editor(ctx.config.editor_command)
      message = editor.Edit(message, ctx.encoding, tmp_prefix='gvnmail.')
      if message is None:
        if not ctx.options.quiet:
          editor.Done()
          sys.stderr.write('not sending')
          sys.stderr.flush()
        return 1
      (toaddrs, message) = ParseMail(pconfig, message)

    if not toaddrs:
      raise gvn.errors.NoMailRecipients

    # Send the user a copy, too.
    toaddrs.append(fromaddr)

    if not ctx.options.quiet:
      sys.stderr.write('sending')
      sys.stderr.flush()

    smtp_conn = smtplib.SMTP(ctx.config.smtp_server)
    # We have to EHLO on our own if we want to use TLS.
    (response_code, response_msg) = smtp_conn.ehlo()
    if not 200 <= response_code <= 299:
      raise gvn.errors.Mail('Bad EHLO response(%d): %s' % (response_code,
                                                           response_msg))
    # If the user requested SMTP authentication, use it if the server
    # supports TLS.
    if (ctx.config.smtp_user is not None
        and ctx.config.smtp_password is not None):
      if smtp_conn.has_extn("starttls"):
        smtp_conn.starttls()
      # Resend EHLO command to retrieve new ESMTP features for TLS.
        smtp_conn.ehlo()
        smtp_conn.login(ctx.config.smtp_user, ctx.config.smtp_password)
      else:
        sys.stderr.write('%s does not support TLS, ignoring smtp_user/password'
                         ' options\n' % (ctx.config.smtp_server,))
    smtp_conn.sendmail(fromaddr, toaddrs, message)
    smtp_conn.quit()

    if editor is not None:
      editor.Done()
      editor = None
  finally:
    if not ctx.options.quiet:
      sys.stderr.write('.\n')
    if editor is not None and not editor.IsDone():
      sys.stderr.write('Your email form was left in a temporary file:\n'
                       '%s\n' % (editor.tmpfile,))

  return 0


options = gvn.cmdline.AuthOptions(
 gvn.cmdline.LogOptions(
  ['change', 'cc', 'editor-cmd', 'force-change',
   'project', 'quiet', 'reviewers'])
)
gvn.cmdline.AddCommand('mail', Handle_GvnMail, helptext__gvn_mail,
                       options, {'change': 'changebranch ARG'},
                       aliases=['syn'])
